JavaScript Event Loop

一、Event Loop

1、过程描述

首先 JavaScript 是单线程的脚本语言,因为假定 JS 同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器就不知道以哪个线程为准。

所以 JavaScript 的运行机制是事件循环(Event Loop),即调用栈【3】不断的向宏任务队列【4】里读取事件。

  • 当执行 JS 脚本的时候,浏览器会把 JS 脚本当成一个宏任务【1】来运行,在执行过程中会产生执行环境,这些执行环境会被顺序的加入到调用栈中。

  • 当执行遇到 setTimeout 之类的宏任务,就把他们推入宏任务队列,在下一轮宏任务执行时调用。

  • 当执行遇到 Promise 之类的微任务【2】,就把他们推入到当前宏任务微任务队列中,在本轮宏任务的同步代码都执行完成后,依次执行所有的微任务
  • 调用栈为空时,调用栈会读取宏任务队列中的任务去执行。

2、概念

【1】宏任务

在 ES3 和更早的版本中,JavaScript 本身还没有异步执行代码的能力,所以宿主环境传递给 JavaScript 引擎一段代码,引擎就把代码直接顺次执行了,这个任务是宿主发起的任务,叫做宏任务。【包括 script、setTimeout、setInterval、MessageChannel、postMessage、setImmediate】

【2】微任务

在 ES5 之后,JavaScript 引入了 Promise,这样不需要浏览器的安排,JavaScript引擎本身也可以发起任务了。 这个任务叫做微观任务。【包括 Promise、process.nextTick、MutationObsever】

【3】调用栈

是一个记录当前程序所在位置的数据结构,如果当前进入了某个函数,这个函数会被放在栈里面,如果当前离开某个函数,这个函数就会被弹出栈外。 即利用这个栈的特殊结构实现同步。js的运行环境有且只有一个调用栈。

【4】宏任务队列

宏任务队列是一个先进先出的数据结构,排在前面的事件,优先被调用栈读取。

3、示例

示例一

1
2
3
4
5
6
7
8
9
console.log('script start')
setTimeout(function () {
console.log('settimeout')
})
console.log('script end')

// script start
// script end
// settimeout

示例二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
console.log('script start')
let promise1 = new Promise(function (resolve) {
console.log('promise1')
resolve()
console.log('promise1 end')
}).then(function () {
console.log('promise2')
})
setTimeout(function () {
console.log('settimeout')
})
console.log('script end')

// script start
// promise1
// promise1 end
// script end
// promise2
// settimeout

// 有时候为了保证先处理完 promise 返回的结果在执行后面的东西,所以会使用 promise 结合 async/await

示例三

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
console.log('1')
setTimeout(function() {
console.log('2')
process.nextTick(function() {
console.log('3')
})
new Promise(function(resolve) {
console.log('4')
resolve()
}).then(function() {
console.log('5')
})
})

process.nextTick(function() {
console.log('6')
})

new Promise(function(resolve) {
console.log('7')
resolve()
}).then(function() {
console.log('8')
})

setTimeout(function() {
console.log('9')
process.nextTick(function() {
console.log('10')
})
new Promise(function(resolve) {
console.log('11')
resolve()
}).then(function() {
console.log('12')
})
})
// 执行结果: 1,7,6,8,2,4,3,5,9,11,10,12

⚠️ Promise 中的异步体现在 then 和 catch 中,所以写在Promise中的代码是被当做同步任务立即执行的

二、Event Loop 进阶

1、过程描述

  • 写在 Promise 中的代码会被当做同步任务立即执行, Promise 中的异步体现在 then 和 catch 中。

  • 出现在 await 之前的代码也是立即执行的,之后的代码异步执行

很多人以为 await 会一直等待之后的表达式执行完之后才继续执行后面的代码,实际上 await 是一个让出线程的标志。await 后面的表达式会先执行一遍,将 await 后面的代码加入到微任务队列中,然后跳出整个 async 函数来执行后面的代码。

2、示例

示例一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}

async function async2() {
console.log('async2')
}

console.log('script start')

setTimeout(() => {
console.log('setTimeout')
}, 0);

async1()

new Promise(resolve => {
console.log('promise1')
resolve()
}).then(() => {
console.log('promise2')
})

console.log('script end')

// script start
// async1 start
// async2
// promise1
// script end
// async1 end
// promise2
// setTimeout
微信打赏